# 定义一个安全气囊期权类
import math

import numpy
import pandas

from option.european_option import EuropeanOption
from option.standard_barrier_options import StandardBarrierOptions


class AirbagOption(object):
    """安全气囊期权类；
    “安全气囊”为自动赎回产品的变种结构。相较于标准自动赎回，安全气囊对于敲入价格的观察更为重要。

    Attributes
    ==========
    S0 : float
        initial stock/index level
    K : float
        strike price
    start_date : string
        起始日期
    end_date : string
        结束日期
    r : float
        constant risk-free short rate
    sigma : float
        volatility factor in diffusion term
    H : float
        barrier level
    barrier_monitoring : string
                观察频率；
                c = continuously，随时观察
                h = hourly，每小时观察
                d = daily，每日观察
                w = weekly，每周观察
                m = monthly，每月观察
                x = 自定义观察日，需指定观察日列表monitoring_date
    participation_rate : float
        敲入参与率
    unparticipation_rate : float
        未敲入参与率
    have_closedtop: bool
        是否有封顶值
    closed_top : float
        封顶值
    monitoring_date : list
        观察日列表，选填，当barrier_monitoring = 'x'时需指定

    Methods
    =======
    valueMC :
        基于蒙特卡洛模拟的方法，返回安全气囊期权的价值
    valueBS :
        基于BS公式计算期权价格方法，有封顶价的安全气囊无法使用本方法；
    deltaMC :
        基于蒙特卡洛模拟方法，返回期权delta值；
    deltaBS :
        基于BS方法，返回期权delta值；
    gammaMC、gammaBS:
        分别基于MC和BS方法返回期权gamma值；
    thetaMC、thetaBS:
        分别基于MC和BS方法返回期权theta值；
    """

    def __init__(self, S0, K, start_date, end_date, r, sigma, H, barrier_monitoring='c', participation_rate=1.0,
                 unparticipation_rate=0.7, have_closedtop=False, closed_top=1.2, *monitoring_date):
        self.S0 = S0
        self.K = K
        self.start_date = start_date
        self.end_date = end_date
        self.r = r
        self.sigma = sigma
        self.H = H
        self.barrier_monitoring = barrier_monitoring
        self.participation_rate = participation_rate
        self.unparticipation_rate = unparticipation_rate
        self.have_closedtop = have_closedtop
        self.closed_top = closed_top
        self.monitoring_date = monitoring_date

        if self.barrier_monitoring == 'x' and self.monitoring_date is None:
            print("Error: Please specify monitoring date list.")
            return



    def valueMC(self, return_rate, n_simulation=10000, seed=1234):
        """基于蒙特卡洛模拟的方法，返回安全气囊期权的价值
        :param return_rate: 标的资产平均收益率，用于模拟标的资产价格
        :param n_simulation: 蒙特卡洛模拟次数，默认为10000次
        :param seed: 随机种子，默认为1234
        :return: 返回安全气囊期权的价值
        """
        # 根据起始时间计算期限T
        start_date = pandas.to_datetime(self.start_date)
        end_date = pandas.to_datetime(self.end_date)
        days = (end_date - start_date).days
        T = days / 365

        # 基于广义维纳过程模拟n_simulation条标的资产路径
        S = GBM(self.S0, return_rate, self.sigma, T, n_simulation, days, seed)

        # 根据观察日索引，抽取对应的标的资产价格
        monitoring_index = get_monitoring_index(self.start_date, self.end_date, self.barrier_monitoring, self.start_date,
                                                *self.monitoring_date)
        monitoring_index = numpy.array(monitoring_index)   # 将monitoring_index转为array
        S_monitoring = S[:, monitoring_index]  # 抽取对应的标的资产价格
        ST = S[:, -1]

        # 根据S_monitoring计算敲入或不敲入时的收益
        option_return = numpy.zeros(n_simulation)
        for i in range(len(S_monitoring)):
            # 若S_monitoring一行中有一个值小于H，则认为敲入，否则不敲入
            if numpy.any(S_monitoring[i] < self.H):
                option_return[i] = (ST[i] - self.K) * self.participation_rate
            else:
                # 若有封顶值，当标的资产价格大于封顶值时，收益为封顶值
                if self.have_closedtop and ST[i] > self.closed_top:
                    option_return[i] = (self.closed_top - self.K) * self.unparticipation_rate
                else:
                    option_return[i] = max((ST[i] - self.K) * self.unparticipation_rate,0)

        airbag_value = numpy.mean(option_return) * math.exp(-self.r * T)
        return airbag_value

    def valueBS(self):
        """
        基于BS公式计算安全气囊期权的价值，本计算方法只适用于无封顶值的情况。
        一个安全气囊可以拆分为两个期权，一个是向下敲入看跌期权，一个是欧式看涨期权。
        :return:期权价值
        """
        if self.have_closedtop:
            print("Error: This method is only applicable to the situation without closed top.")
            return

        T = (pandas.to_datetime(self.end_date) - pandas.to_datetime(self.start_date)).days / 365

        # 先计算向下敲入看跌期权的价值
        pdi = StandardBarrierOptions('pdi', self.S0, self.K, T, self.r, self.sigma, self.H)
        pdi_value = pdi.value()

        # 再计算欧式看涨期权的价值
        call_option = EuropeanOption('c', self.S0, self.K, T, self.r, self.sigma)
        call_value = call_option.value()

        airbag_value = call_value*self.unparticipation_rate  - pdi_value *self.participation_rate

        return airbag_value

    def delta_BS(self):
        """
        基于BS公式，将S0微小变动，计算安全气囊期权的delta
        :return: 返回安全气囊期权的delta
        """
        dS = 0.0001
        # 先将原价值保存
        original_value = self.valueBS()
        # S0微小变动
        self.S0 += dS
        delta = (self.valueBS() - original_value) / dS
        self.S0 -= dS
        return delta

    def delta_MC(self, return_rate, n_simulation=10000, seed=1234):
        """
        基于蒙特卡洛模拟的方法，将S0微小变动，计算安全气囊期权的delta
        :param return_rate: 标的资产平均收益率，用于模拟标的资产价格
        :param n_simulation: 蒙特卡洛模拟次数，默认为10000次
        :param seed: 随机种子，默认为1234
        :return: 返回安全气囊期权的delta
        """
        dS = 0.0001
        # 先将原价值保存
        original_value = self.valueMC(return_rate, n_simulation, seed)
        # S0微小变动
        self.S0 += dS
        delta = (self.valueMC(return_rate, n_simulation, seed) - original_value) / dS
        self.S0 -= dS
        return delta

    def gamma_BS(self):
        """
        基于BS公式，将S0微小变动，delta的变动幅度。
        :return: 返回安全气囊期权的gamma
        """
        ds = 0.0001
        # 先将原delta保存
        original_delta = self.delta_BS()
        # S0微小变动
        self.S0 += ds
        delta = self.delta_BS()
        # 计算gamma
        gamma = (delta - original_delta) / ds
        self.S0 -= ds
        return gamma

    def gamma_MC(self, return_rate, n_simulation=10000, seed=1234):
        """
        基于蒙特卡洛模拟的方法，将S0微小变动，计算安全气囊期权的gamma
        :param return_rate: 标的资产平均收益率，用于模拟标的资产价格
        :param n_simulation: 蒙特卡洛模拟次数，默认为10000次
        :param seed: 随机种子，默认为1234
        :return: 返回安全气囊期权的gamma
        """
        ds = 0.0001
        # 先将原delta保存
        original_delta = self.delta_MC(return_rate, n_simulation, seed)
        # S0微小变动
        self.S0 += ds
        delta = self.delta_MC(return_rate, n_simulation, seed)
        # 计算gamma
        gamma = (delta - original_delta) / ds
        self.S0 -= ds

        return gamma

def GBM(S0, r, sigma, T, n_simulation, n_steps, seed=1234):
    """
    根据几何布朗运动模型模拟标的资产价格
    :param S0: 标的资产初始价格
    :param r: 无风险利率
    :param sigma: 波动率
    :param T: 期限
    :param n_simulation: 模拟次数
    :param n_steps: 模拟步数
    :param seed: 随机种子
    :return: 返回模拟的标的资产价格
    """
    numpy.random.seed(seed)
    dt = T/n_steps
    S = numpy.zeros((n_simulation, n_steps+1))
    S[:, 0] = S0
    for i in range(n_steps):
        z = numpy.random.standard_normal(n_simulation)
        S[:, i+1] = S[:, i] * numpy.exp((r-0.5*sigma**2)*dt + sigma*math.sqrt(dt)*z)
    return S


def get_monitoring_index(start_day, end_day, barrier_monitoring, today, *monitoring_date):
    """
    根据观察频率计算从当前时间，计算需要观察的索引
    :param start_day: 产品开始日期
    :param end_day: 产品结束日期
    :param barrier_monitoring:观察类型
                观察频率；
                c = continuously，随时观察
                h = hourly，每小时观察
                d = daily，每日观察
                w = weekly，每周观察
                m = monthly，每月观察
                x = 自定义观察日，需指定观察日列表monitoring_date
    :param today: 当前时间，是计算观察索引的起点，起始时间为0
    :param monitoring_date: 若观察类型为x，则需指定观察日列表。
    :return: 返回观察索引列表
    """
    # 根据观察日频率计算对应的需要观察的索引
    start_date = pandas.to_datetime(start_day)
    end_date = pandas.to_datetime(end_day)
    today = pandas.to_datetime(today)
    today_to_end = (end_date - today).days
    start_to_today = (today - start_date).days

    if barrier_monitoring == 'c' or barrier_monitoring == 'h' or barrier_monitoring == 'd':
        monitoring_index = numpy.arange(0, today_to_end + 1)
    elif barrier_monitoring == 'w':
        mod_days = 7 - start_to_today % 7
        monitoring_index = numpy.arange(mod_days, today_to_end + 1, 7)
    elif barrier_monitoring == 'm':
        mod_days = 30 - start_to_today % 30
        monitoring_index = numpy.arange(mod_days, today_to_end + 1, 30)
    elif barrier_monitoring == 'x':
        # 将观察日列表中的日期减去起始日期，得到对应的索引
        monitoring_date = [pandas.to_datetime(i) for i in monitoring_date]
        # 再将列表中的日期减去起始日期，得到对应的索引
        monitoring_index = [(i - today).days for i in monitoring_date]
        # 去除小于0的索引
        monitoring_index = [i for i in monitoring_index if i >= 0]
    else:
        print("Error: Barrier monitoring type not valid.")
        return

    # 将monitoring_index转为list
    monitoring_index = list(monitoring_index)
    return monitoring_index